/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { h, fragment } from '@knno/dom/jsx';
import { Nodes, dom } from '@knno/dom';
import { editorStyle } from './rich/inlineEditor.css';
import { DragMask } from '../components/mask/mask';
import {
	parseTable,
	calculateCells,
	createTable,
	tableOnContextMenu,
	CellInfo,
	tableOnContextMenuMultipleCells,
	getAllColumnWidth,
	getCellInfo,
	setAllColumnWidth,
	tableOnKeyDown,
} from './rich/table';
import { paragraphOnContextMenu, paragraphOnKeyDown } from './rich/paragraph';
import { createImage, imageOnKeyDown } from './rich/image';
import { EventBus, isDescendant } from '@knno/ui/utils';
import { askInsertImage, askInsertTable } from '../tools/ui';
import {
	convertToDataURL,
	Editor,
	EditorProps,
	findCursorElement,
	findParent,
	getRtfImages,
	isBRElement,
	isImageElement,
	isParagraphElement,
	isTableElement,
	isTextNode,
	loadImageToDataURL,
	TrimNodeContext,
} from './editor/editor';

export type ToolbarData = {
	editor?: InlineEditor;
	activeElements?: HTMLElement[];
};

export type EditCommand = {
	toolbarChange: ToolbarData | undefined;
};

export const TOOLBAR_CHANGE = 'toolbarChange';

export type EditorEventBus = EventBus<EditCommand>;

export interface InlineEditorProps extends EditorProps<InlineEditor> {
	// data: any;
	// field: string;
	source?: string;
	eventBus?: EditorEventBus;
}

class Rich extends HTMLElement {
	connectedCallback() {
		this.dispatchEvent(new Event('connect'));
	}
	disconnectedCallback() {
		this.dispatchEvent(new Event('disconnect'));
	}
}
customElements.define('rich-editor', Rich);

const isFirefox = window.navigator.userAgent.indexOf('Firefox') >= 0;

export class InlineEditor extends Editor<InlineEditor> {
	private eventBus: EditorEventBus | undefined;
	private selectedTable: HTMLTableElement | undefined;
	private selectedCells: CellInfo[] = [];

	onSelectionChange(): boolean {
		const elem = this.getActived();
		if (elem) {
			this.refreshToolbar();
			return true;
		} else {
			return false;
		}
	}

	constructor(attrs: InlineEditorProps) {
		const src = attrs?.source;
		// let subscriber:
		// 	| {
		// 			unSubscribe(): void;
		// 	  }
		// 	| undefined;
		super(
			<rich-editor
				contenteditable={isFirefox ? 'true' : 'plaintext-only'}
				class={editorStyle}
				oninput={() => {
					this.emitChange();
				}}
				// ondisconnect={() => {
				// 	subscriber?.unSubscribe();
				// }}
				// onconnect={() => {
				// 	subscriber = attrs.eventBus?.subscribe('selectionChange', () => {
				// 		this.refreshToolbar();
				// 	});
				// }}
			>
				{src ? (
					dom.html(src)
				) : (
					<p>
						<br />
					</p>
				)}
			</rich-editor>,
			attrs,
			[]
		);
		this.eventBus = attrs.eventBus;
		let beginCell: HTMLTableCellElement | undefined;
		let endCell: HTMLTableCellElement | undefined;
		let selectMode = false;
		this.on('keyup', (evt) => {
			if ((evt.key === 'Backspace' || evt.key === 'Delete') && !evt.ctrlKey && !evt.altKey) {
				const root = this.elems()[0];
				if ([...root.children].filter((c) => c.nodeName.toLowerCase() !== 'br').length === 0) {
					root.querySelectorAll(':scope>br').forEach((br) => root.removeChild(br));
					const p = (
						<p>
							<br />
						</p>
					).elem()!;
					root.appendChild(p);
					const sel = window.getSelection();
					if (!sel) return;
					const range = document.createRange();
					range.selectNodeContents(p);
					range.collapse();
					sel.removeAllRanges();
					sel.addRange(range);
				}
			}
		});
		this.on('keydown', (evt) => {
			const target = evt?.target as HTMLElement | null;
			if (!target) return;
			const elem = findCursorElement();
			if (!elem) {
				if (evt.key === 'Backspace' || evt.key === 'Delete') {
					this.selectedCells.forEach((cell) => {
						if (cell.td) {
							cell.td.innerHTML = '<br>';
						}
					});
					this.emitChange();
				}
				return;
			}
			if (evt.key === 'a' && evt.ctrlKey) {
				if (!this.isSelectAll()) {
					this.selectAll();
					evt.preventDefault();
					evt.stopPropagation();
				}
				return;
			}
			const root = this.elems()[0];
			if (elem.nodeName === 'P' && elem.parentElement === root) {
				if (evt.key === 'v' && evt.ctrlKey) {
					// evt.preventDefault();
					// evt.stopPropagation();
					// this.paste().then(() => this.emitChange());
				} else {
					paragraphOnKeyDown(this, elem as HTMLParagraphElement, evt);
				}
			} else if (elem.nodeName === 'DIV' && elem.parentElement === root) {
				imageOnKeyDown(this, elem as HTMLParagraphElement, evt);
			} else if (elem.nodeName === 'TD') {
				if (evt.key === 'Enter' && evt.ctrlKey) {
					evt.preventDefault();
					const tb = findParent<HTMLTableElement>(elem, 'table');
					if (!tb) return null;
					const sel = window.getSelection();
					if (!sel || sel.rangeCount === 0) return null;
					sel.empty();
					const range = document.createRange();
					range.selectNode(tb);
					range.collapse(evt.shiftKey);
					const p = (
						<p>
							<br />
						</p>
					).elems()[0];
					range.insertNode(p);
					sel.addRange(range);
					range.selectNodeContents(p);
					range.collapse();
				} else {
					tableOnKeyDown(this, elem as HTMLTableCellElement, evt);
				}
			} else if (elem === root) {
				if (evt.key === 'Enter') {
					evt.preventDefault();
					const sel = window.getSelection();
					if (!sel || sel.rangeCount === 0) return null;
					const range = sel.getRangeAt(0);
					range.insertNode(
						(
							<p>
								<br />
							</p>
						).elems()[0]
					);
					range.collapse();
				} else if (evt.key === 'Backspace') {
					const sel = window.getSelection();
					if (!sel || sel.rangeCount === 0) return null;
					const range = sel.getRangeAt(0);
					if (
						range.startContainer === range.endContainer &&
						range.startOffset === range.endOffset &&
						root.childNodes[range.startOffset]?.nodeName === 'DIV'
					) {
						evt.preventDefault();
						evt.preventDefault();
						const div = root.childNodes[range.startOffset] as HTMLElement;
						if (div.previousElementSibling) {
							if (div.previousElementSibling.nodeName !== 'P') {
								range.selectNode(div.previousElementSibling);
								range.deleteContents();
							} else {
								if (div.previousElementSibling.textContent === '') {
									range.selectNode(div.previousElementSibling);
									range.deleteContents();
								} else {
									range.selectNodeContents(div.previousElementSibling);
									range.collapse(false);
								}
							}
						}
					}
				}
				this.emitChange();
			}
		});
		this.on('contextmenu', (evt) => {
			evt.preventDefault();
			const target = evt?.target as HTMLElement | null;
			if (!target) return;
			const elem = findCursorElement();
			if (!elem) return;
			if (elem.nodeName === 'P') {
				paragraphOnContextMenu(this, elem as HTMLParagraphElement, evt);
			} else if (elem.nodeName === 'TD') {
				tableOnContextMenu(this, target as HTMLTableCellElement, evt);
			}
		});

		// const tableInfoCache = new Map<HTMLTableElement, TableInfo>();
		// function findTableInfo(table: HTMLTableElement): TableInfo {
		// 	let info = tableInfoCache.get(table);
		// 	if (!info) {
		// 		info = parseTable(table);
		// 		tableInfoCache.set(table, info);
		// 	}
		// 	return info;
		// }
		this.on('pointerdown', (evt) => {
			beginCell = undefined;
			endCell = undefined;
			selectMode = false;
			if (evt.button === 2 && this.selectedCells.length > 0) {
				this.eventBus?.send(TOOLBAR_CHANGE, {
					editor: this,
					activeElements: this.selectedCells.filter((cell) => cell.td).map((cell) => cell.td as HTMLElement),
				});
				tableOnContextMenuMultipleCells(this, this.selectedCells, evt, () => {
					this.refreshToolbar();
				});
				evt.preventDefault();
				return;
			}
			this.clearTableSelection();
			const target = evt?.target as HTMLElement | null;
			if (!target || evt.button !== 0) return;
			if (target.nodeName === 'TD') {
				const table = findParent<HTMLTableElement>(target, 'table') ?? undefined;
				if (table) {
					const tableInfo = parseTable(table);
					// const table = findParent<HTMLTableElement>(target, 'table');
					const cellInfo = getCellInfo(tableInfo, target as HTMLTableCellElement);
					if (cellInfo) {
						if (evt.offsetX < 5 || evt.offsetX > target.offsetWidth - 5) {
							const tableColumnWidth = getAllColumnWidth(tableInfo);
							let left: number;
							let right: number;
							let leftWidth: number;
							let rightWidth: number;
							if (evt.offsetX < 5) {
								left = cellInfo.column - 1;
								right = cellInfo.column;
							} else {
								left = cellInfo.column;
								right = cellInfo.column + 1;
								if (right >= tableColumnWidth.length) {
									right = -1;
								}
							}
							if (left >= 0) {
								leftWidth = tableColumnWidth[left];
							}
							if (right >= 0) {
								rightWidth = tableColumnWidth[right];
							}
							// console.log('columns:', left, right);
							// console.log('src width:', tableColumnWidth);
							DragMask.show(evt, 'col-resize', (ev, distance) => {
								if (left >= 0) {
									tableColumnWidth[left] = leftWidth + distance.x;
								}
								if (right >= 0) {
									tableColumnWidth[right] = rightWidth - distance.x;
								}
								// console.log(tableColumnWidth);
								setAllColumnWidth(tableInfo, tableColumnWidth);
								this.emitChange();
							});
							evt.preventDefault();
							return;
						}
					}
				}
				this.selectedTable = findParent<HTMLTableElement>(target, 'table') ?? undefined;
				if (this.selectedTable) {
					beginCell = target as HTMLTableCellElement;
				}
			}
		});
		this.on('pointermove', (evt) => {
			const target = evt?.target as HTMLElement | null;
			if (!target) return;
			if (target.nodeName === 'TD' && beginCell) {
				endCell = target as HTMLTableCellElement;
				const table = findParent<HTMLTableElement>(target, 'table') ?? undefined;
				if (table && table === this.selectedTable && (endCell !== beginCell || selectMode)) {
					selectMode = true;
					const selection = window.getSelection();
					if (selection && selection.rangeCount > 0) {
						selection.removeAllRanges();
					}
					table.querySelectorAll('td').forEach((td) => (td.className = ''));
					this.selectedCells = calculateCells(parseTable(table), [beginCell, endCell]);
					this.selectedCells.forEach((cell) => {
						if (cell.td) {
							cell.td.className = 'selected';
						}
					});
					this.eventBus?.send(TOOLBAR_CHANGE, {
						editor: this,
						activeElements: this.selectedCells.filter((cell) => cell.td).map((cell) => cell.td as HTMLElement),
					});
				}
			} else if (target.nodeName === 'TD') {
				const table = findParent<HTMLTableElement>(target, 'table') ?? undefined;
				if (table) {
					// const table = findParent<HTMLTableElement>(target, 'table');
					if (evt.offsetX < 5 || evt.offsetX > target.offsetWidth - 5) {
						table.className = 'change-column';
					} else {
						table.className = '';
					}
				}
			}
		});
		this.on('pointerup', (evt) => {
			const target = evt?.target as HTMLElement | null;
			if (!target) return;
			beginCell = undefined;
			endCell = undefined;
			selectMode = false;
			if (target.nodeName === 'TD') {
				endCell = target as HTMLTableCellElement;
			}
		});
		this.on('dragover', (evt) => {
			evt.preventDefault();
			const elem = findCursorElement();
			if (!elem) return;
			const root = this.elems()[0];
			if (elem.nodeName === 'P' && elem.parentElement === root) {
				if (evt.dataTransfer) {
					evt.dataTransfer.dropEffect = 'copy';
				}
			} else {
				if (evt.dataTransfer) {
					evt.dataTransfer.dropEffect = 'none';
				}
			}
		});
		this.on('drop', (evt) => {
			evt.preventDefault();
			const elem = findCursorElement();
			if (!elem) return;
			const root = this.elems()[0];
			if (elem.nodeName === 'P' && elem.parentElement === root) {
				const clipboardData = evt.dataTransfer;
				if (!clipboardData) return;
				this.paste(clipboardData).then(() => this.emitChange());
			}
		});
		this.on('paste', (evt) => {
			const elem = findCursorElement();
			if (!elem) return;
			const root = this.elems()[0];
			if (elem.nodeName === 'P' && elem.parentElement === root) {
				evt.preventDefault();
				evt.stopPropagation();
				const clipboardData = evt.clipboardData;
				if (!clipboardData) return;
				this.paste(clipboardData).then(() => this.emitChange());
			}
		});
	}

	private insertNode(node: Nodes, range: Range, p: HTMLElement) {
		range.deleteContents();
		range.setEndAfter(p);
		const text = range.extractContents()?.textContent ?? '';
		const content = node.appendTo(this.elems(), p);
		if (p.textContent === '') {
			dom.wrap(p).detach();
		}
		if (text) {
			(<p>{text}</p>).appendTo(this.elems(), content);
		}
	}

	insertTable() {
		const elem = findCursorElement(false);
		if (!elem) return;
		if (elem.nodeName !== 'P') return;
		const sel = window.getSelection();
		if (!sel || sel.rangeCount === 0) return;
		const range = sel.getRangeAt(0);
		if (!range) return;
		askInsertTable((rows, cols) => {
			this.insertNode(createTable(rows, cols), range, elem);
			this.emitChange();
		});
	}

	insertImage() {
		const elem = findCursorElement(false);
		if (!elem) return;
		if (elem.nodeName !== 'P') return;
		const sel = window.getSelection();
		if (!sel || sel.rangeCount === 0) return;
		const range = sel.getRangeAt(0);
		if (!range) return;
		askInsertImage((src) => {
			const imgNode = createImage(src);
			this.insertNode(imgNode, range, elem);
			this.emitChange();
		});
	}

	deleteTable(table: HTMLTableElement) {
		table.parentElement?.removeChild(table);
		if (this.children.length === 0) {
			const p = (
				<p>
					<br />
				</p>
			);
			this.append(p);
			const selection = window.getSelection();
			const range = document.createRange();
			range.selectNodeContents(p.elem() as HTMLElement);
			range.collapse(false);
			selection?.removeAllRanges();
			selection?.addRange(range);
			this.emitChange();
		}
	}

	// paste html content should be trimed to allowed style.
	async paste(clipboardData: DataTransfer) {
		//const win32 = YDY.app.getSysInfo().deviceInfo.osType === 'win32';
		const elem = findCursorElement(false);
		if (!elem) return;
		if (elem.nodeName !== 'P') return;
		const sel = window.getSelection();
		if (!sel || sel.rangeCount === 0) return;
		const range = sel.getRangeAt(0);
		range.deleteContents();
		range.collapse();
		function trimNode(node: Node, context: TrimNodeContext) {
			if (/^(style|script|meta|title|head|#comment)$/i.test(node.nodeName)) return;
			if (isTextNode(node) || isBRElement(node)) {
				if (context.pureText) {
					context.pureTextFragment.appendChild(node);
				} else {
					if (context.paragraph) {
						context.paragraph.appendChild(node);
					} else {
						const p = document.createElement('p');
						p.appendChild(node);
						context.destFragment.appendChild(p);
						context.paragraph = p;
					}
				}
			} else if (isTableElement(node)) {
				context.pureText = false;
				context.paragraph = undefined;
				[...node.tBodies].forEach((section) => {
					[...section.rows].forEach((row) => {
						[...row.attributes].forEach((attr) => row.removeAttribute(attr.name));
						[...row.cells].forEach((cell) => {
							if (cell.nodeName.toLowerCase() === 'th') {
								const td = document.createElement('td');
								td.textContent = cell.textContent;
								if (td.childNodes.length === 0) {
									td.appendChild(document.createElement('br'));
								}
								[...cell.attributes]
									.filter((attr) => /^(rowspan|colspan)$/i.test(attr.name))
									.forEach((attr) => td.setAttribute(attr.name, attr.value));
								row.insertBefore(td, cell);
								row.removeChild(cell);
							} else {
								cell.textContent = cell.textContent ?? '';
								if (cell.childNodes.length === 0) {
									cell.appendChild(document.createElement('br'));
								}
								[...cell.attributes]
									.filter((attr) => !/^(rowspan|colspan)$/i.test(attr.name))
									.forEach((attr) => cell.removeAttribute(attr.name));
							}
						});
						node.appendChild(row);
					});
					node.removeChild(section);
				});
				[...node.attributes].forEach((attr) => node.removeAttribute(attr.name));
				context.destFragment.appendChild(node);
			} else if (isImageElement(node)) {
				context.pureText = false;
				context.paragraph = undefined;
				[...node.attributes].filter((attr) => attr.name != 'src').forEach((attr) => node.removeAttribute(attr.name));
				// convert image src
				if (!/^data:/.test(node.src)) {
					const url = node.src;
					node.src = '';
					if (url.startsWith('file:///')) {
						const blob = context.images.shift();
						if (blob) {
							// node.src = blob;
							convertToDataURL(blob)
								.then((dataUrl) => (node.src = dataUrl))
								.catch((reason) => console.error(reason));
						}
					} else {
						loadImageToDataURL(url)
							.then((dataUrl) => (node.src = dataUrl))
							.catch((reason) => console.error(reason));
					}
				}
				node.style.maxWidth = '100%';
				const div = document.createElement('div');
				div.appendChild(node);
				context.destFragment.appendChild(div);
			} else if (isParagraphElement(node)) {
				context.pureText = false;
				const p = document.createElement('p');
				context.destFragment.appendChild(p);
				context.paragraph = p;
				node.childNodes.forEach((child) => trimNode(child, context));
			} else {
				node.childNodes.forEach((child) => trimNode(child, context));
			}
		}
		try {
			//const items = await navigator.clipboard.read();
			const context = {
				pureTextFragment: document.createDocumentFragment(),
				destFragment: document.createDocumentFragment(),
				pureText: true,
				images: [],
			};
			if (clipboardData.types.includes('text/html')) {
				if (clipboardData.types.includes('text/rtf')) {
					getRtfImages(clipboardData.getData('text/rtf'), context);
				}
				const html = clipboardData.getData('text/html');
				const fragment = range.createContextualFragment(html);
				[...fragment.childNodes].forEach((node) => trimNode(node, context));
			} else if (clipboardData.types.includes('text/plain')) {
				const text = clipboardData.getData('text/plain');
				trimNode(document.createTextNode(text), context);
			}
			for (const file of clipboardData.files) {
				if (/^image\/(png|jpeg|svg\+xml)$/.test(file.type)) {
					const url = await convertToDataURL(file);
					trimNode(
						(
							<div>
								<img src={url} />
							</div>
						).elems()[0],
						context
					);
				}
			}
			if (context.pureTextFragment.hasChildNodes()) {
				range.insertNode(context.pureTextFragment);
				range.collapse();
			}
			if (context.destFragment.hasChildNodes()) {
				range.setEnd(range.startContainer, range.startContainer.childNodes.length);
				const otherPart = range.extractContents();
				const firstElement = context.destFragment.firstChild;
				if (firstElement?.nodeName === 'P') {
					firstElement.appendChild(otherPart);
				} else {
					const newParagraph = document.createElement('P');
					newParagraph.appendChild(otherPart);
					context.destFragment.insertBefore(newParagraph, context.destFragment.firstChild);
				}
				range.setEndAfter(elem);
				range.collapse();
				range.insertNode(context.destFragment);
				range.collapse();
			}
		} catch (err) {
			console.error('Paste failed:', err);
		}
	}

	clearTableSelection() {
		this.selectedCells = [];
		this.selectedTable?.querySelectorAll('td')?.forEach((td) => (td.className = ''));
	}

	getActived(): HTMLElement | null {
		const elem = findCursorElement();
		this.clearTableSelection();
		if (!elem || !isDescendant(this.elems(), elem)) {
			return null;
		} else {
			return elem;
		}
	}

	isSelectAll(): boolean {
		const sel = window.getSelection();
		if (!sel || sel.rangeCount === 0) return false;
		const range = sel.getRangeAt(0);
		return (
			range.startContainer === this.elem() &&
			range.endContainer === this.elem() &&
			range.startOffset === 0 &&
			range.endOffset === this.elem()?.childNodes?.length
		);
	}

	selectAll() {
		const rootElement = this.elem();
		if (!rootElement) return;
		const sel = window.getSelection();
		if (!sel) return;
		sel.removeAllRanges();
		const range = document.createRange();
		range.selectNodeContents(rootElement);
		sel.addRange(range);
	}

	refreshToolbar() {
		const elem = this.getActived();
		if (elem) {
			this.eventBus?.send(TOOLBAR_CHANGE, { editor: this, activeElements: [elem] });
		}
	}

	private updateTimer: NodeJS.Timeout | undefined;
	emitChange() {
		if (this.updateTimer) {
			clearTimeout(this.updateTimer);
		}
		this.updateTimer = setTimeout(() => {
			this.updateTimer = undefined;
			// this.attrs.data[this.attrs.field] = this.html();
			this.emit(new Event('change', { bubbles: true }));
		}, 50);
	}
}
